fix(multichain-account-service): stop BaseBip44AccountProvider leaking removed accounts and stale account IDs - #10069
Open
gomesalexandre wants to merge 3 commits into
Conversation
…h BaseBip44AccountProvider getAccounts() cast the AccountsController's (InternalAccount | undefined)[] response straight through, so an account ID this provider still tracks but the AccountsController no longer knows about (e.g. it was removed) silently produced an 'account' that was actually undefined at runtime. init() was purely additive, so a re-init after a removal (e.g. every unlock in metamask-mobile's Authentication flow, which calls MultichainAccountService.init() -> provider.init() on each call) left the removed account's ID tracked forever. Fix both: filter undefined entries out of getAccounts(), and make init() replace the tracked account set instead of adding to it.
…h BaseBip44AccountProvider getAccounts() cast the AccountsController's (InternalAccount | undefined)[] response straight through, so an account ID this provider still tracks but the AccountsController no longer knows about (e.g. it was removed) silently produced an 'account' that was actually undefined at runtime. init() was purely additive, so a re-init after a removal (e.g. every unlock in metamask-mobile's Authentication flow, which calls MultichainAccountService.init() -> provider.init() on each call) left the removed account's ID tracked forever. Fix both: filter undefined entries out of getAccounts(), and make init() replace the tracked account set instead of adding to it. Also update the shared test-provider mock (tests/providers.ts) to model the same replace-not-add init() semantics, so higher-level service/wallet tests don't silently exercise the old (buggy) behavior via the mock.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it says on the box
BaseBip44AccountProviderhas two related bugs that both stem from the same root cause: its internal account-ID tracking (this.accounts) can silently drift from what theAccountsControlleractually knows about.1.
getAccounts()returnsundefinedentries cast as if they were real accountsAccountsController.getAccounts(accountIds)is documented as returning(InternalAccount | undefined)[]— one slot per requested ID,undefinedfor any ID it doesn't recognize (e.g. the account was removed).BaseBip44AccountProvider#getAccounts()cast this straight through:The sibling
MultichainAccountGroup#getAccounts()already guards this exact case (if (account) { ... } // might mean it has been deleted), butBaseBip44AccountProvider's own batch method didn't.2.
init()is purely additive, so a re-init after a removal leaves stale IDs foreverMultichainAccountService#init()callsprovider.init(state)on every provider, computed fresh from current account state, every time it runs. It's not a one-shot boot call —metamask-mobile'sAuthentication.tscallsMultichainAccountService.init()on every unlock:So: remove an account, lock, unlock — the provider re-inits with a fresh (smaller) list, but the old ID is still in
this.accountsbecauseinit()only ever adds.Receipts
Real regression tests added (
BaseBip44AccountProvider.test.ts), using a mockAccountsController:getAccountshandler that mirrors the real.map()-based contract (same-length array,undefinedin place for missing IDs — not a shorter, filtered array, which is what happened to make this invisible toEvmAccountProvider.test.ts's existing mock).Genuine red-before/green-after via
git stashon the source fix alone:Full package suite:
Test Suites: 17 passed, 17 total/Tests: 353 passed, 353 total, 100% statement/function/line coverage, 96.85% branch (unchanged from baseline).yarn lint:tsc(the real monorepo-widetsc --build, not a scoped package check, which silently no-ops for this package since it has no locallint:tscscript) andyarn eslinton all changed files both clean.Also updated the shared
tests/providers.tsmock provider'sinit()implementation to match the real replace-not-add semantics — it's used by higher-levelMultichainAccountService/MultichainAccountWallettests, and was otherwise silently modeling the old buggy behavior even after this fix. Full suite re-confirmed green after that change too (nothing relied on the old mock behavior).Fix
undefinedentries out ofgetAccounts()instead of casting them through.init()now doesthis.accounts = new Set(accounts)— replaces rather than adds.Review
Reviewed adversarially with Codex (
codex exec, synchronous). It found no production correctness bug in the intended fix, but caught two real issues in my first pass, both fixed:KeyringCapabilitiesfrom the wrong subpath (@metamask/keyring-apiinstead of@metamask/keyring-api/v2) — a realTS2305, only visible under the monorepo's realtsc --build, not the scoped package check I'd run first.tests/providers.tsmock'sinit()was still additive, inconsistent with the fix (described above, fixed).Note
Medium Risk
Touches core multichain account ID tracking used on every unlock/re-init; behavior change is intentional but could affect alignment or account listing if callers relied on additive init or undefined entries.
Overview
Fixes account drift in
BaseBip44AccountProviderwhen theAccountsControllerdrops an account but the provider’s internal ID set has not caught up yet (e.g. after remove + lock/unlock re-init).getAccounts()now dropsundefinedslots fromAccountsController:getAccountsinstead of casting them through as real accounts.init()replaces the tracked ID set (new Set(accounts)) instead of only adding IDs, so repeated inits with a smaller list no longer leave removed IDs tracked (includingisAlignedfalse positives).Adds
BaseBip44AccountProvider.test.tswith a mock that mirrors the controller’s per-IDundefinedcontract, and aligns the shared test mock’sinitwith replace semantics.Reviewed by Cursor Bugbot for commit 9fae227. Bugbot is set up for automated code reviews on this repo. Configure here.